using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Client.AspNetCore;
using System.Security.Claims;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Net8.Identity.MvcClient.Controllers
{
    public class AuthController : Controller
    {
        private readonly string _issuer;
        
        public AuthController(IConfiguration configuration)
        {
            _issuer = configuration["OpenIddict:Issuer"] ?? "https://localhost:7031/";
        }
        
        /// <summary>
        /// 登录操作
        /// </summary>
        /// <param name="returnUrl">登录成功后返回的URL</param>
        /// <returns>挑战结果，重定向到身份提供者</returns>
        [HttpGet("~/login")]
        public ActionResult LogIn(string returnUrl)
        {
            var items = new Dictionary<string, string>
            {
                // 注意：当客户端选项中只注册了一个客户端时，
                // 设置颁发者属性不是必需的，可以省略。
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = _issuer
            };
            var properties = new AuthenticationProperties(items)
            {
                // 只允许本地返回URL以防止打开重定向攻击。
                RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
            };

            // 要求OpenIddict客户端中间件将用户代理重定向到身份提供者。
            return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 退出登录操作
        /// </summary>
        /// <param name="returnUrl">退出登录后返回的URL</param>
        /// <returns>登出结果</returns>
        [HttpPost("~/logout"), ValidateAntiForgeryToken]
        public async Task<ActionResult> LogOut(string returnUrl)
        {
            // 检索存储在本地身份验证cookie中的身份。如果它不可用，
            // 这表示用户已在本地注销（或尚未登录）。
            var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            if (result is not { Succeeded: true })
            {
                // 只允许本地返回URL以防止打开重定向攻击。
                return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");
            }

            // 在触发重定向到远程服务器之前删除本地身份验证cookie。
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            var properties = new AuthenticationProperties(new Dictionary<string, string>
            {
                // 注意：当客户端选项中只注册了一个客户端时，
                // 设置颁发者属性不是必需的，可以省略。
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = _issuer,
                // 虽然不是必需的，但规范鼓励发送id_token_hint
                // 包含服务器为此用户返回的身份令牌的参数。
                [OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] = result.Properties.GetTokenValue(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken)
            })
            {
                // 只允许本地返回URL以防止打开重定向攻击。
                RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/"
            };

            // 请求OpenIddict客户端中间件将用户代理重定向至身份提供者
            return SignOut(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        /// <summary>
        /// 登录回调操作
        /// </summary>
        /// <param name="provider">身份提供者</param>
        /// <returns>登录结果</returns>
        /// <exception cref="InvalidOperationException">当外部授权数据不能用于身份验证时抛出</exception>
        [HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
        public async Task<IActionResult> LogInCallbackAsync(string provider)
        {
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
            if (result.Principal.Identity is not ClaimsIdentity { IsAuthenticated: true })
            {
                throw new InvalidOperationException("外部授权数据不能用于身份验证");
            }

            var claims = result.Principal.Claims
                .Select(claim => claim switch
                {
                    { Type: Claims.Subject } => new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer),
                    { Type: Claims.Email } => new Claim(Claims.Email, claim.Value, claim.ValueType, claim.Issuer),
                    { Type: Claims.Name } => new Claim(Claims.Name, claim.Value, claim.ValueType, claim.Issuer),
                    _ => null
                })
                .Where(claim => claim != null)
                .GroupBy(claim => claim.Type)
                .Select(group => group.First())
                .ToList();

            var identity = new ClaimsIdentity(
                claims,
                authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
                nameType: Claims.Name,
                roleType: Claims.Role);

            var properties = new AuthenticationProperties(items: result.Properties.Items);
            properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
            {
                // 保留令牌响应中返回的访问、身份和刷新令牌(如果可用)。
                {
                    Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
                } => true,
                // 忽略其他令牌。
                _ => false
            }));

            // 请求Cookie身份验证处理程序返回新的Cookie，
            // 并将用户代理重定向到身份验证属性中存储的返回URL。
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme, 
                new ClaimsPrincipal(identity), properties);
            
            return Redirect(properties.RedirectUri);
        }

        /// <summary>
        /// 退出登录回调操作
        /// </summary>
        /// <param name="provider">身份提供者</param>
        /// <returns>退出登录结果</returns>
        [HttpGet("~/callback/logout/{provider}"), HttpPost("~/callback/logout/{provider}"), IgnoreAntiforgeryToken]
        public async Task<IActionResult> LogOutCallbackAsync(string provider)
        {
            // 注意：此控制器对所有提供程序使用相同的回调操作，但对于喜欢对每个提供程序使用不同操作的用户，可以将以下操作拆分为单独的操作。
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
            
            // 在此示例中，总是在用户代理重定向到授权服务器之前删除本地身份验证Cookie。
            // 喜欢延迟删除本地Cookie的应用程序可以从注销操作中删除相应的代码，
            // 并在此操作中删除身份验证Cookie。
            return Redirect(result!.Properties!.RedirectUri);
        }
    }
}